| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- 'use client';
- import { useEffect, useRef, useState, useCallback } from 'react';
- import { useSignalRContext } from '@/contexts/signalrProvider';
- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
- import { faGift, faXmark } from '@fortawesome/free-solid-svg-icons';
- import './donation-toast.scss';
- type ToastItem = {
- id: number;
- content: string;
- };
- type Props = {
- channelSID: string;
- };
- const AUTO_DISMISS_MS = 6000;
- const MAX_VISIBLE = 3;
- let nextId = 0;
- /**
- * 후원/시스템 알림 토스트 (우상단).
- *
- * - SignalR `ReceiveSystemMessage` 를 수신하여 토스트로 노출
- * - 기존 dpot SignalR 채팅에 표시되던 후원 알림 메시지를 대체
- * - YouTube iframe 사용 환경에서 dpot 시스템 메시지 노출 채널 역할
- *
- * 자동 dismiss: AUTO_DISMISS_MS 후 사라짐. 수동 닫기 버튼 제공.
- */
- export default function DonationToast({ channelSID }: Props)
- {
- const { chatConnection, chatConnected } = useSignalRContext();
- const [toasts, setToasts] = useState<ToastItem[]>([]);
- const timersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());
- const removeToast = useCallback((id: number) => {
- setToasts(prev => prev.filter(t => t.id !== id));
- const timer = timersRef.current.get(id);
- if (timer) {
- clearTimeout(timer);
- timersRef.current.delete(id);
- }
- }, []);
- useEffect(() => {
- if (!chatConnection || !chatConnected) {
- return;
- }
- // 채널 참가 (시스템 메시지 수신을 위해 필수)
- chatConnection.invoke('JoinChannel', channelSID).catch((err) => {
- console.error('[DonationToast] 채널 참가 실패:', err);
- });
- const handler = (content: string) => {
- const id = ++nextId;
- setToasts(prev => {
- const next = [...prev, { id, content }];
- // 동시 노출 제한: 가장 오래된 것부터 제거
- if (next.length > MAX_VISIBLE) {
- const removed = next.shift();
- if (removed) {
- const timer = timersRef.current.get(removed.id);
- if (timer) {
- clearTimeout(timer);
- timersRef.current.delete(removed.id);
- }
- }
- }
- return next;
- });
- const timer = setTimeout(() => {
- setToasts(prev => prev.filter(t => t.id !== id));
- timersRef.current.delete(id);
- }, AUTO_DISMISS_MS);
- timersRef.current.set(id, timer);
- };
- chatConnection.on('ReceiveSystemMessage', handler);
- return () => {
- chatConnection.off('ReceiveSystemMessage', handler);
- if (chatConnection.state === 'Connected') {
- chatConnection.invoke('LeaveChannel').catch(() => {});
- }
- };
- }, [chatConnection, chatConnected, channelSID]);
- // 컴포넌트 언마운트 시 모든 타이머 정리
- useEffect(() => {
- const timers = timersRef.current;
- return () => {
- timers.forEach(timer => clearTimeout(timer));
- timers.clear();
- };
- }, []);
- if (toasts.length === 0) {
- return null;
- }
- return (
- <div className="donation-toast" role="region" aria-live="polite" aria-label="후원 알림">
- {toasts.map(toast => (
- <div key={toast.id} className="donation-toast__item">
- <span className="donation-toast__icon" aria-hidden="true">
- <FontAwesomeIcon icon={faGift} />
- </span>
- <span className="donation-toast__content">{toast.content}</span>
- <button
- type="button"
- className="donation-toast__close"
- onClick={() => removeToast(toast.id)}
- aria-label="알림 닫기"
- >
- <FontAwesomeIcon icon={faXmark} />
- </button>
- </div>
- ))}
- </div>
- );
- }
|